import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Base64;

public class EncryptDemo {

    public static String byteToHexString(byte[] bytes) {
        StringBuilder resultHexString = new StringBuilder();
        String tempStr;
        for (byte b: bytes) {
            //这里需要对b与0xff做位与运算，
            //若b为负数，强制转换将高位位扩展，导致错误，
            //故需要高位清零
            tempStr = Integer.toHexString(b & 0xff);
            //若转换后的十六进制数字只有一位，
            //则在前补"0"
            if (tempStr.length() == 1) {
                resultHexString.append(0).append(tempStr);
            } else {
                resultHexString.append(tempStr);
            }
        }
        return resultHexString.toString();
    }


    public static String makeMD5FromString(String oStr) {
        String strRtn = null;
        try {
            // https://blog.csdn.net/qq_41437844/article/details/121120227
            MessageDigest md5 = MessageDigest.getInstance("MD5");

            byte[] digestBtyes = md5.digest(oStr.getBytes(StandardCharsets.UTF_8));
            strRtn  = byteToHexString(digestBtyes);

        }catch (Exception e) {
            e.printStackTrace();
        }

        return strRtn;
    }

    public static String makeMD5FromFile(String filePath) {

        // https://www.cnblogs.com/ShakeTian/articles/15241863.html

        String strRtn = null;
        try {
            byte[] buffer = new byte[4096];
            int len = 0;
            MessageDigest md5 = MessageDigest.getInstance("MD5");
            File f = new File(filePath);
            FileInputStream fis = new FileInputStream(f);
            while ((len = fis.read(buffer)) != -1) {
                md5.update(buffer, 0, len);
            }
            fis.close();
            byte[] digestBtyes = md5.digest();

            strRtn  = byteToHexString(digestBtyes);

        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

        return strRtn;
    }

    public static String poem = """
七律·长征AB
毛泽东
1935年10月
红军不怕远征难，万水千山只等闲
五岭逶迤腾细浪，乌蒙磅礴走泥丸
金沙水拍云崖暖，大渡桥横铁索寒
更喜岷山千里雪，三军过后尽开颜
""";

    protected static void md5Demo() {
        // 获取字符串 md5摘要
        System.out.println("poem : " + poem);
        String poemMD5 = makeMD5FromString(poem);
        System.out.println("poemMD5 : " + poemMD5);

        // 获取文件 md5摘要
        String workingDIRPath = FileDemo.currentWorkdingDir();
        String targetFile = workingDIRPath + File.separator + "ggDemo.jar";

        System.out.println("targetFile : " + targetFile);
        String fileMD5 = makeMD5FromFile(targetFile);
        System.out.println("fileMD5 : " + fileMD5);

    }
    public static String makeBase64Encode(byte[] bytes) {
        byte[] rtnStrBytes = Base64.getEncoder().encode(bytes);

        String strRtn = new String(rtnStrBytes, StandardCharsets.UTF_8);

        return strRtn;
    }
    public static String makeBase64Encode(String oStr) {

        byte[] rtnStrBytes = Base64.getEncoder().encode(oStr.getBytes(StandardCharsets.UTF_8));

        String strRtn = new String(rtnStrBytes, StandardCharsets.UTF_8);

        return strRtn;
    }

    public static String makeBase64Decode(String base64Str) {
        // gzip bytes --> base64 bytes
        byte[] rtnStrBytes = Base64.getDecoder().decode(base64Str.getBytes(StandardCharsets.UTF_8));
        // base64 bytes --> gzip string
        String strRtn = new String(rtnStrBytes, StandardCharsets.UTF_8);

        return strRtn;
    }

    protected static void base64Demo() {

        System.out.println("poem : " + poem);

        // 获取字符串 base64 编码
        String poemBase64 = makeBase64Encode(poem);
        System.out.println("poemBase64 : " + poemBase64);

        // 获取字符串 base64 解码
        String decodeStr = makeBase64Decode(poemBase64);
        System.out.println("decodeStr : " + decodeStr);
    }

    /**
     * 合并数组
     *
     * @param firstArray  第一个数组
     * @param secondArray 第二个数组
     * @return 合并后的数组
     */
    public static byte[] concat(byte[] firstArray, byte[] secondArray) {
        if (firstArray == null || secondArray == null) {
            return null;
        }
        byte[] bytes = new byte[firstArray.length + secondArray.length];
        System.arraycopy(firstArray, 0, bytes, 0, firstArray.length);
        System.arraycopy(secondArray, 0, bytes, firstArray.length,
                secondArray.length);
        return bytes;
    }

    /*
        AES/CBC/NOPaddin
        AES 默认模式
        使用CBC模式, 在初始化Cipher对象时, 需要增加参数, 初始化向量IV : IvParameterSpec iv = new
        IvParameterSpec(key.getBytes());
        NOPadding: 使用NOPadding模式时, 原文长度必须是8byte的整数倍   ECB模式是可重复解密的

        PKCS5Padding（默认）
        在明文的末尾进行填充，填充的数据是当前和16个字节相差的数量，例如：
        未填充明文
        1，2，3，4，5，6，7，8，9，10，11

        填充明文（缺少五个满足16个字节）
        1，2，3，4，5，6，7，8，9，10，11，5，5，5，5，5

        　由于使用PKCS7Padding/PKCS5Padding填充时，最后一个字节肯定为填充数据的长度，所以在解密后可以准确删除填充的数据

        ** ISO10126Padding
        在明文的末尾进行填充，当前和16个字节相差的数量填写在最后，其余字节填充随机数，例如：
        未填充明文
        1，2，3，4，5，6，7，8，9，10，11

        填充明文（缺少五个满足16个字节）
        1，2，3，4，5，6，7，8，9，10，11，c，b，4，1，5
    */
    //public static final String transformation = "AES/ECB/NOPadding";
    public static final String transformation = "AES/ECB/PKCS5Padding";

    public static int byteToInt(byte b) {
        return (b) & 0xff;
    }

    public static byte[] makePKCS5PaddingRemove(byte[] bytesPadding) {
        if (bytesPadding.length == 0) {
            return bytesPadding;
        }
        int paddingNum = byteToInt(bytesPadding[bytesPadding.length-1]);
        if (paddingNum >= bytesPadding.length) {
            // 不是按协议 PKCS5Padding 填充的字节数组
            return bytesPadding;
        }
        for (int index=bytesPadding.length-1; index > bytesPadding.length-1 - paddingNum; --index) {
            if (paddingNum != byteToInt(bytesPadding[index])) {
                // PKCS5Padding 协议要求 在明文的末尾进行填充，填充的数据是当前和16个字节相差的数量
                // 不是按协议 PKCS5Padding 填充的字节数组
                return bytesPadding;
            }
        }

        // 检查确认 bytesPadding 是按协议 PKCS5Padding 填充的字节数组

        byte[] bytes = new byte[bytesPadding.length - paddingNum];
        System.arraycopy(bytesPadding, 0, bytes, 0, bytesPadding.length - paddingNum);
        return bytes;
    }

    public static byte[] makePKCS5Padding(byte[] bytesCheck2Padding) {
        /*
            PKCS5Padding（默认）
            在明文的末尾进行填充，填充的数据是当前和16个字节相差的数量，例如：
            未填充明文
            1，2，3，4，5，6，7，8，9，10，11
            填充明文（缺少五个满足16个字节）
            1，2，3，4，5，6，7，8，9，10，11，5，5，5，5，5
            由于使用PKCS7Padding/PKCS5Padding填充时，最后一个字节肯定为填充数据的长度，所以在解密后可以准确删除填充的数据
        */

        //不足16字节，补齐内容为差值; 如果正好是16字节对齐的，len的值为 16，表示补充16个值为16的byte在最后
        int len = 16 - bytesCheck2Padding.length % 16;
        if (len > 0) {
            System.out.println("makePKCS5Padding : input.length= "+bytesCheck2Padding.length+", 16字节补齐 " + len);

            byte[] append = new byte[len];
            for (int i = 0; i < len; i++) {
                append[i] = (byte) len;
            }
            byte[] dataAppended = concat(bytesCheck2Padding, append);

            System.out.println("makePKCS5Padding : 补齐后，dataAppended.length = " + dataAppended.length);

            return dataAppended;
        }

        return bytesCheck2Padding;
    }


    /**
     * 获取补齐的位置
     *
     * @param paddingBytes 源数组
     * @return 补齐的位置
     */
    private static int paddingIndex(byte[] paddingBytes) {
        for (int i = paddingBytes.length - 1; i >= 0; i--) {
            if (paddingBytes[i] != 0) {
                return i + 1;
            }
        }
        return -1;
    }

    // 获取字符串 aes 加密编码（16位加密key）
    public static String makeAESEncrypt(String oStr, String key16Char) {

        try {
            byte[] data = oStr.getBytes("UTF-8");
            byte[] dataPadding = makePKCS5Padding(data);
            System.out.println("makeAESEncrypt paddingIndex = " +paddingIndex(dataPadding));

            SecretKeySpec key = new SecretKeySpec(key16Char.getBytes(), "AES");

            // AES 加密
            Cipher cipher = Cipher.getInstance(transformation);
            cipher.init(Cipher.ENCRYPT_MODE, key);
            byte[] result = cipher.doFinal(dataPadding);

            // 字节数组，编码成base64字符串输出
            return makeBase64Encode(result);

        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    public static String makeAESDecrypt(String encodeEncryptStr, String key16Char) {
        try {
            // base64 解码

            byte[] enctryptBytes = Base64.getDecoder().decode(encodeEncryptStr.getBytes(StandardCharsets.UTF_8));
            System.out.println("makeAESDecrypt : enctryptBytes.length = " + enctryptBytes.length);


            SecretKeySpec key = new SecretKeySpec(key16Char.getBytes(), "AES");

            //
            Cipher cipher = Cipher.getInstance(transformation);
            cipher.init(Cipher.DECRYPT_MODE, key);
            byte[] decryptResult = cipher.doFinal(enctryptBytes);
            System.out.println("makeAESDecrypt : 解密后 result.length = " + decryptResult.length);
            System.out.println("makeAESDecrypt paddingIndex = " +paddingIndex(decryptResult));

            // 加密的时候 原始信息字节数组 已经按 PKCS5Padding 协议做了填充，解密后，看起来得到是 按 PKCS5Padding 填充后的字节数组
            byte[] originBytes =makePKCS5PaddingRemove(decryptResult);
            System.out.println("makeAESDecrypt originBytes.length = " + originBytes.length);
            return new String(originBytes, StandardCharsets.UTF_8);

        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    protected static void AESDemo() {
        // 获取字符串 aes 加密编码（16位加密key）
        String aesEncode = makeAESEncrypt(poem, "JavaDemoJavaDemo");
        System.out.println("aesEncode : " + aesEncode);

        String aesDecode = makeAESDecrypt(aesEncode, "JavaDemoJavaDemo");
        System.out.println("aesDecode : " + aesDecode);
    }

    protected static void demoLogic() {
        DateTimeDemo.logTs("md5Demo");
        md5Demo();

        DateTimeDemo.logTs("base64Demo");
        base64Demo();

        DateTimeDemo.logTs("AESDemo");
        AESDemo();

    }
}
